#include <stdio.h>
#include <string.h>

#include "src/ltb_dep.h"
#include "src/ltb_type.h"
#include "src/ltdstr.h"
#include "src/lterr.h"
#include "src/ltlog.h"
#include "src/ltopt.h"
#include "src/ltprint_arr.h"

typedef struct {
    lta_Strings features;
    bool help;
} ltb_CommandLine;

static void
AddRule(ltb_Config* cfg, ltb_Rule rule, char const* cmd, ...) {
    lta_Append(rule.cmd, cmd);

    va_list ap;
    va_start(ap, cmd);
    for (;;) {
        char const* arg = va_arg(ap, char const*);
        if (arg == NULL)
            break;
        lta_Append(rule.cmd, arg);
    }
    va_end(ap);

    lta_Append(cfg->rules, rule);
}

static void
AddDefine(ltb_Config* cfg, char const* name, ...) {
    ltb_Define define = {.name = name};

    va_list ap;
    va_start(ap, name);
    for (;;) {
        char const* value = va_arg(ap, char const*);
        if (value == NULL)
            break;
        lta_Append(define.value, value);
    }
    va_end(ap);

    lta_Append(cfg->defines, define);
}

static void
WriteDefine(ltb_Define const* define, char const* prefix) {
    printf("%s%s =", prefix, define->name);

    lta_ForEach(i, define->value) { printf(" %s", define->value.data[i]); }
    printf("\n");
}

static void
WriteCfg(ltb_Config const* cfg) {
    printf("# Defines\n");
    lta_ForEach(define_i, cfg->defines) {
        ltb_Define const* define = &cfg->defines.data[define_i];

        WriteDefine(define, "");
    }
    printf("\n");

    printf("# Rules\n");
    lta_ForEach(rule_i, cfg->rules) {
        ltb_Rule const* rule = &cfg->rules.data[rule_i];

        printf("rule %s\n", rule->name);
        printf("  command =");
        lta_ForEach(cmd_i, rule->cmd) { printf(" %s", rule->cmd.data[cmd_i]); }
        printf("\n");

        if (rule->depfile != NULL)
            printf("  depfile = %s\n", rule->depfile);
    }
    printf("\n");

    printf("# Builds\n");
    lta_ForEach(build_i, cfg->builds) {
        ltb_Build const* build = &cfg->builds.data[build_i];

        printf("build");
        lta_ForEach(i, build->out) { printf(" %s", build->out.data[i]); }
        printf(": %s", build->rule);

        lta_ForEach(i, build->in) { printf(" %s", build->in.data[i]); }
        printf("\n");

        lta_ForEach(i, build->defines) {
            WriteDefine(&build->defines.data[i], "  ");
        }
    }
}

static void CollectCflagsOfDeps(ltb_Config* cfg, lta_U32Arr const* deps,
                                lta_Strings* cflags);

static ltdstr_String AddBuildCc(ltb_Config* cfg, char const* srcpath,
                                lta_Strings const* cflags);

static bool
AddExecutable(ltb_Config* cfg, Target const* target, lta_U32Arr const* deps) {
    lta_Strings cflags = {0};
    CollectCflagsOfDeps(cfg, deps, &cflags);

    ltdstr_String binpath = ltdstr_NewArenaString(&cfg->arena);

    ltdstr_Append(&binpath, "$bindir/");
    ltdstr_Append(&binpath, target->name);

    ltb_Build link = {.rule = "link"};
    lta_Append(link.out, ltdstr_Get(&binpath));

    lta_ForEach(i, target->srcs) {
        char const* srcpath = target->srcs.data[i];
        ltdstr_String obj = AddBuildCc(cfg, srcpath, &cflags);
        lta_Append(link.in, ltdstr_Get(&obj));
    }

    lta_ForEach(i, *deps) {
        Target const* dep = &cfg->targets.data[deps->data[i]];
        if (dep->type != ltb_Library)
            continue;

        ltdstr_String libpath = ltdstr_NewArenaString(&cfg->arena);

        ltdstr_Append(&libpath, "$libdir/lib");
        ltdstr_Append(&libpath, dep->name);
        ltdstr_Append(&libpath, ".a");

        lta_Append(link.in, ltdstr_Get(&libpath));
    }

    lta_Append(cfg->builds, link);
    return true;
}

static void
CollectCflagsOfDeps(ltb_Config* cfg, lta_U32Arr const* deps,
                    lta_Strings* cflags) {
    lta_ForEach(i, *deps) {
        Target const* dep = &cfg->targets.data[deps->data[i]];

        if (dep->type == ltb_Cflags) {
            lta_ForEach(cflags_i, dep->flags) {
                lta_Append(*cflags, dep->flags.data[cflags_i]);
            }
        }
    }
}

static ltdstr_String
AddBuildCc(ltb_Config* cfg, char const* srcpath, lta_Strings const* cflags) {
    ltdstr_String obj = ltdstr_NewArenaString(&cfg->arena);

    ltdstr_Append(&obj, "$objdir/");
    ltdstr_Append(&obj, srcpath);
    ltdstr_Append(&obj, ".o");

    ltb_Build build = {.rule = "cc"};
    lta_Append(build.in, srcpath);
    lta_Append(build.out, ltdstr_Get(&obj));

    ltb_Define define_cflags = {.name = "cflags"};
    lta_ForEach(cflags_i, *cflags) {
        lta_Append(define_cflags.value, cflags->data[cflags_i]);
    }
    lta_Append(build.defines, define_cflags);

    lta_Append(cfg->builds, build);

    return obj;
}

static bool
AddLibrary(ltb_Config* cfg, Target const* library, lta_U32Arr const* deps) {
    lta_Strings cflags = {0};
    CollectCflagsOfDeps(cfg, deps, &cflags);

    ltdstr_String libpath = ltdstr_NewArenaString(&cfg->arena);

    ltdstr_Append(&libpath, "$libdir/lib");
    ltdstr_Append(&libpath, library->name);
    ltdstr_Append(&libpath, ".a");

    ltb_Build build_lib = {.rule = "ar"};
    lta_Append(build_lib.out, ltdstr_Get(&libpath));

    lta_ForEach(i, library->srcs) {
        char const* srcpath = library->srcs.data[i];
        ltdstr_String obj = AddBuildCc(cfg, srcpath, &cflags);
        lta_Append(build_lib.in, ltdstr_Get(&obj));
    }

    lta_Append(cfg->builds, build_lib);

    return true;
}

static bool
AddTarget(ltb_Config* cfg, Target const* target) {
    /* 防止重定义目标。*/
    uint32_t target_id;
    if (ltb_FindTarget(cfg, target->name, &target_id)) {
        ltlog_Log1(Error, "redefined target", "library name",
                   StringQuote(target->name));
        return false;
    }

    /* 收集目标的依赖。*/
    lta_U32Arr deps = {0};
    if (!ltb_CollectDeps(cfg, target, &deps, ltb_IsTop)) {
        ltlog_Log1(Error, "failed to collect deps",
                   "target:", String(target->name));
        return false;
    }

    if (ltlog_IsOn(ltlog_Info)) {
        lta_Strings dep_names = {0};

        lta_ForEach(i, deps) {
            Target const* dep = &cfg->targets.data[deps.data[i]];
            lta_Append(dep_names, dep->name);
        }

        ltlog_Log2(Info, "find deps", "target", StringQuote(target->name),
                   "deps", LtaStrings(&dep_names));
    }

    /* 增加目标到cfg。*/
    lta_Append(cfg->targets, *target);

    switch (target->type) {
        case ltb_Library: return AddLibrary(cfg, target, &deps);
        case ltb_Executable: return AddExecutable(cfg, target, &deps);

        /* 其他情况不需要做什么。*/
        default: return true;
    }
}

static lterr_Code
OnArgument(void* ctx, char opt_name, char const* opt) {
    ltb_CommandLine* cli = (ltb_CommandLine*)ctx;

    switch (opt_name) {
        case 'f': lta_Append(cli->features, opt); break;
        case 'h': cli->help = true; break;
        default:
            ltlog_Log1(Error, "invalid option", "option:", CharQuote(opt_name));
            return lterr_kError;
    }

    return lterr_kOk;
}

static void
DefineLdflags(ltb_Config* cfg, lta_Strings const* features) {
    ltb_Define it = {.name = "ldflags"};

    lta_ForEach(i, *features) {
        if (strcmp(features->data[i], "asan") == 0) {
            lta_Append(it.value, "-lasan");
        }
    }

    lta_Append(cfg->defines, it);
}

static void
DefineCflags(ltb_Config* cfg, lta_Strings const* features) {
    ltb_Define it = {.name = "cflags"};

    lta_ForEach(i, *features) {
        if (strcmp(features->data[i], "asan") == 0) {
            lta_Append(it.value, "-fsanitize=address");
        }
    }

    lta_Append(cfg->defines, it);
}

int
main(int argc, char const** argv) {
    ltb_CommandLine cli = {0};
    lterr_Code code = ltopt_Parse(argc, argv, "f", OnArgument, &cli);

    if (code != 0) {
        ltlog_Log1(Error, "failed to parse command line",
                   "error:", String(lterr_Name(code)));
        return 1;
    }

    if (cli.help) {
        ltopt_OptionHelp opthelp[] = {
            {.name = 'f', .help = "Open a build feature"},
            {.name = 'h', .help = "Print this help and exit"},
            {.name = 0},
        };

        ltopt_Usage usage = {.name = "ltb",
                             .help = "Generate build.ninja for lt",
                             .options = opthelp};

        ltopt_WriteUsage(&usage, stderr);
        return 1;
    }

    ltb_Config cfg = {0};

    AddDefine(&cfg, "cc", "gcc", NULL);
    AddDefine(&cfg, "ar", "ar", NULL);

    DefineCflags(&cfg, &cli.features);
    DefineLdflags(&cfg, &cli.features);

    AddDefine(&cfg, "builddir", "build", NULL);

    AddDefine(&cfg, "objdir", "$builddir/obj", NULL);
    AddDefine(&cfg, "bindir", "$builddir/bin", NULL);
    AddDefine(&cfg, "libdir", "$builddir/lib", NULL);

    AddRule(&cfg, (ltb_Rule){.name = "cc", .depfile = "$out.d"}, "$cc", "-MD",
            "-MF", "$out.d", "$cflags", "-o", "$out", "-c", "$in", NULL);
    AddRule(&cfg, (ltb_Rule){.name = "ar"}, "$ar", "rs", "$out", "$in", NULL);
    AddRule(&cfg, (ltb_Rule){.name = "link"}, "$cc", "$ldflags", "-o", "$out",
            "$in", NULL);

    {
        Target target = {.name = "cflags", .type = ltb_Cflags};

        lta_Append(target.flags, "-Wall");
        lta_Append(target.flags, "-Wextra");
        lta_Append(target.flags, "-Wpedantic");
        lta_Append(target.flags, "-O2");
        lta_Append(target.flags, "-I.");
        lta_Append(target.flags, "-g");

        AddTarget(&cfg, &target);
    }

    {
        Target target = {.name = "lt", .type = ltb_Library};

        lta_Append(target.srcs, "src/lta.c");
        lta_Append(target.srcs, "src/ltarena.c");
        lta_Append(target.srcs, "src/lterr.c");
        lta_Append(target.srcs, "src/ltini.c");
        lta_Append(target.srcs, "src/ltio.c");
        lta_Append(target.srcs, "src/ltlexer.c");
        lta_Append(target.srcs, "src/ltlog.c");
        lta_Append(target.srcs, "src/ltopt.c");
        lta_Append(target.srcs, "src/ltpanic.c");
        lta_Append(target.srcs, "src/ltprint.c");
        lta_Append(target.srcs, "src/ltt.c");
        lta_Append(target.srcs, "src/ltt_testcase.c");

        lta_Append(target.public_deps, "cflags");

        AddTarget(&cfg, &target);
    }

    {
        Target it = {.name = "ltx", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ltx.c");
        lta_Append(it.srcs, "src/ltx_ls.c");
        lta_Append(it.srcs, "src/ltx_cat.c");
        lta_Append(it.srcs, "src/ltx_ini_show.c");

        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    {
        Target it = {.name = "ugly_number", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ugly_number.c");
        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    {
        Target it = {.name = "ltb", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ltb.c");
        lta_Append(it.srcs, "src/ltb_dep.c");
        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    {
        Target it = {.name = "ltt", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ltt_main.c");
        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    {
        Target it = {.name = "ltnewc", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ltnewc.c");
        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    {
        Target it = {.name = "ltgen", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ltgen.c");
        lta_Append(it.srcs, "src/ltgen_gens.c");
        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    {
        Target it = {.name = "ltlexer_show", .type = ltb_Executable};

        lta_Append(it.srcs, "src/ltlexer_show.c");
        lta_Append(it.secret_deps, "lt");

        AddTarget(&cfg, &it);
    }

    WriteCfg(&cfg);
    return 0;
}
